#!/usr/bin/env python
# -*- coding: utf-8 -*-

#
# This program is free software; you can redistribute it and/or modify  
# it under the terms of the GNU General Public License as published by  
# the Free Software Foundation; either version 2 of the License, or     
# (at your option) any later version.                                   
#                                                                         
# A copy of the license can be found in the license.txt file supplied   
# with this software or at: http://www.gnu.org/copyleft/gpl.html       
#

import os,os.path,sys


import celtuce.help.logging as logging

from celler import Celler

from helper import CELL_CONFIG_FILE,CELL_DIR_MODE,saveCellConfig,loadCellConfig,makeCellPath
from consts import CELL_VOLUME
from celtuce.event import Event, OP_RESPONSE,OP_SHUTDOWN 
from celtuce.event import OP_CREATE_CELL,OP_SPLIT_CELL,OP_INSERT,OP_FIND,OP_ERASE,OP_ASSIGN_CELL,OP_INSERTUNIQUE,OP_UPDATE,OP_UPSERT
from celtuce.event import EVENT_NEW_CELL_BORN,EVENT_NEW_CELL_DEVICE
from celtuce.event import ROUTE_ADD_CELL,ROUTE_DEL_CELL
from celtuce.response import Response,RC_OK,RC_INTERNAL_ERROR,RC_OUT_OF_CELL_INTERVAL,RC_CELL_ALREADY_EXISTS


from slb64db import Slb64DB as CellDB


class   Worker(Celler) :
    def __init__(self, keeper, dev_id=0) :
        Celler.__init__(self)

        self._keeper_ref = keeper
        self.dev_id = dev_id

        self._dbs = {}

        self._queue_wait_response = {}

        self._is_alive = False
        self._logger = logging.getLogger('worker')
        ## __init__()

    def startup(self) :
        # do some initialization


        self.start()
        self._is_alive = True
        ## startup()


    def shutdown(self) :
        for db_path,cell_tuple in self._dbs.iteritems() :
            celldb,cell_key_begin,cell_key_end = cell_tuple
            celldb.shutdown()
        self._dbs = {}

        self._is_alive = False
        ## shutdown()

    def isAlive(self) :
        return self._is_alive
        ## isAlive()

    def handle(self, evt) :
        self._logger.debug('worker handle %s', str(evt))

        # handle request
        if evt.op in (OP_INSERT, OP_INSERTUNIQUE, OP_UPDATE, OP_UPSERT)  :
            response = self.__doInsertOrUpdate(evt.body, evt.op)
        elif OP_FIND == evt.op :
            response = self.find(evt.body)
        elif OP_ERASE == evt.op :
            response = self.erase(evt.body)

        elif OP_CREATE_CELL == evt.op :
            response = self.createCell(evt.body)
        elif OP_ASSIGN_CELL == evt.op :
            response = self.assignCell(evt.body)

        # handle response
        elif OP_RESPONSE == evt.op :
            self.handleResponse(evt)
            return
        elif OP_SHUTDOWN == evt.op :
            self.shutdown()
            return

        elif OP_SPLIT_CELL == evt.op :
            self.splitCell(evt.body)
            return

        else :
            raise RuntimeError,'Why not handle event "%s"' % str(evt)

        # send response back
        evt.response_to = evt.request_id
        evt.body = response
        evt.request_id = self.genRequestID()
        evt.op = OP_RESPONSE

        #queue_response = evt.source_queue
        evt.source_queue = self

        return evt
        ## handle()

    def handleResponse(self, evt) :
        evt_source = self._queue_wait_response.pop(evt.response_to, None)
        if not evt_source :
            # not found source event
            return

        ## handleResponse()

    def splitCell(self, args) :
        table_name, db_path, new_dev_id, new_db_root_path = args

        celldb,cell_key_begin,cell_key_end = self.getCellDB( db_path )
        if celldb.getApproxVolume() < CELL_VOLUME :
            # just ignore some duplicate event
            return

        self._logger.info('split table "%s" cell %s (volume:%.2f GB) ...',table_name,db_path,celldb.getApproxVolume() / 1073741824.0)
        split_key,tmp_cell_path = celldb.split(table_name, new_db_root_path, CELL_VOLUME)
        if split_key :
            split_a_cell_path = db_path
            split_a_config_file = os.sep.join((split_a_cell_path, CELL_CONFIG_FILE))
            _tbl_name,_key_begin,_key_end = loadCellConfig( split_a_config_file )
            self._logger.info('split table %s cell (%s .. %s] into (%s .. %s]  and (%s .. %s]', _tbl_name,repr(_key_begin),repr(_key_end),repr(_key_begin),repr(split_key),repr(split_key),repr(_key_end))

            # change dirname of tablet cell
            split_b_cell_path = makeCellPath(new_db_root_path, table_name, _key_begin, split_key)
            self._logger.info('rename %s --> %s ...', tmp_cell_path, split_b_cell_path)
            os.rename(tmp_cell_path, split_b_cell_path)

            split_b_config_file = os.sep.join((split_b_cell_path, CELL_CONFIG_FILE))

            # update cell config
            saveCellConfig(split_a_config_file, table_name, split_key, _key_end)
            saveCellConfig(split_b_config_file, table_name, _key_begin, split_key)

            # post ROUTE_DEL_CELL
            evt_del_cell = Event()
            evt_del_cell.request_id = self.genRequestID()
            evt_del_cell.source_queue = self
            evt_del_cell.op = ROUTE_DEL_CELL
            evt_del_cell.body = [(table_name, db_path, _key_begin, _key_end),]
            self.postEventToKeeper( evt_del_cell )

            # post EVENT_NEW_CELL_BORN 
            evt_born = Event()
            evt_born.request_id = self.genRequestID()
            evt_born.source_queue = self
            evt_born.op = EVENT_NEW_CELL_BORN
            evt_born.body = (table_name, split_b_cell_path, _key_begin, split_key)
            self.postEventToKeeper( evt_born )

            # post ROUTE_ADD_CELL
            evt_add_cell = Event()
            evt_add_cell.request_id = self.genRequestID()
            evt_add_cell.source_queue = self
            evt_add_cell.op = ROUTE_ADD_CELL
            evt_add_cell.body = [(table_name, split_a_cell_path, split_key, _key_end),(table_name, split_b_cell_path, _key_begin,split_key)]
            self.postEventToKeeper( evt_add_cell )

            # drop old cell-metadata from local route table(_dbs)
            del self._dbs[ db_path ]

            self._logger.info('split table "%s" cell %s done.',table_name,db_path)
        else :
            self._logger.error('split table "%s" cell %s failed.',table_name,db_path)

        ## splitCell()

    ####-----------------------------------------------------------####

    def createCell(self, args) :
        cell = args

        try :
            # make dirs
            if os.path.exists(cell.path) :
                table_name,key_begin,key_end = loadCellConfig(cell.path)
                if not (table_name == cell.table and key_begin == cell.key_begin and key_end == cell.key_end) :
                    response = Response()
                    response.status_code = RC_CELL_ALREADY_EXISTS
                    response.error = "create cell %s(%r .. %r] conflicted with exists cell %s(%r .. %r]" % (cell.table, cell.key_begin, cell.key_end, table_name, key_begin, key_end)
                    return response
            else :
                os.makedirs(cell.path, CELL_DIR_MODE)

                # create configure
                saveCellConfig(cell.path, cell.table, cell.key_begin, cell.key_end)
        except :
            self._logger.excpt()
            response = Response()
            response.status_code = RC_INTERNAL_ERROR
            t, v, tb = sys.exc_info()
            response.error = '%s -- "%s"' % (str(t), str(v))
            return response

        return Response()
        ## createCell()

    def assignCell(self, args) :
        cell = args

        try :
            # make dirs
            if not os.path.exists(cell.path) :
                raise RuntimeError,'Cell path "%s" does NOT exist' % cell.path

            # load configure
            table_name,key_begin,key_end = loadCellConfig(cell.path)

            # check consist of configure
            if not (table_name == cell.table and key_begin == cell.key_begin and key_end == cell.key_end) :
                raise RuntimeError,'Cell config "%s(%s .. %s]" != Cell assign "%s"' % (table_name,key_begin,key_end, str(cell))

            # load celldb
            if cell.path in self._dbs :
                cell_tuple = self._dbs.pop( cell.path )
                celldb,cell_key_begin,cell_key_end = cell_tuple
                celldb.shutdown()
                del celldb

            self.__loadCellDB( cell.path )
        except :
            self._logger.excpt()
            response = Response()
            response.status_code = RC_INTERNAL_ERROR
            t, v, tb = sys.exc_info()
            response.error = '%s -- "%s"' % (str(t), str(v))
            return response

        return Response()
        ## assignCell()

    ####-----------------------------------------------------------####

    def postEventToKeeper(self, evt) :
        self._keeper_ref.handle(evt)
        ## postEventToKeeper()

    def getCellDB(self, db_path) :
        return self.__loadCellDB( db_path )
        ## getCellDB()

    def checkSplitableCell(self, celldb, table_name, db_path) :
        '''

        @return true if cell is split, false if not split.
        '''
        if celldb.getApproxVolume() > CELL_VOLUME :
            if celldb.isCompactable() :
                celldb.compact()

                return False
            else :
                evt = Event()
                evt.request_id = self.genRequestID()
                evt.source_queue = self
                evt.op = EVENT_NEW_CELL_DEVICE
                evt.body = (table_name, db_path)
                self.postEventToKeeper( evt )

                return True

        return False
        ## checkSplitableCell()

    def __loadCellDB(self, db_path) :
        if db_path in self._dbs :
            return self._dbs[ db_path ]

        table_name,key_begin,key_end = loadCellConfig( db_path )
        celldb = CellDB( db_path )
        self._dbs[ db_path ] = (celldb, key_begin, key_end)
        return (celldb, key_begin, key_end)
        ## __loadCellDB()

    ####-----------------------------------------------------------####

    def __doInsertOrUpdate(self, args, op) :
        #self._logger.debug('Insert:%s',repr(args))
        table_name, key, value, timestamp_ms, db_path = args

        # STEP. get celldb
        celldb,cell_key_begin,cell_key_end = self.getCellDB( db_path )

        # STEP. check key interval
        if not (key > cell_key_begin and key <= cell_key_end) :
            response = Response(RC_OUT_OF_CELL_INTERVAL)
            response.error = 'Key %s is out of interval of table "%s" cell (%s .. %s]' % (repr(key),table_name,repr(cell_key_begin),repr(cell_key_end))
            return response

        # STEP. check whether to split celldb or not
        splitted = self.checkSplitableCell( celldb, table_name, db_path )
        if splitted :
            # if split occured, re-check again
            celldb,cell_key_begin,cell_key_end = self.getCellDB( db_path )

            if not (key > cell_key_begin and key <= cell_key_end) :
                response = Response(RC_OUT_OF_CELL_INTERVAL)
                response.error = 'Key %s is out of interval of table "%s" cell (%s .. %s]' % (repr(key),table_name,repr(cell_key_begin),repr(cell_key_end))
                self._logger.error('%s', response.error)
                return response
        
        # STEP. execute insert operation on celldb
        try :
            if OP_INSERT == op :
                ok = celldb.insert(key, value, timestamp_ms)
            elif OP_INSERTUNIQUE == op :
                ok = celldb.insertunique(key, value, timestamp_ms)
            elif OP_UPDATE == op :
                ok = celldb.update(key, value, timestamp_ms)
            elif OP_UPSERT == op :
                ok = celldb.upsert(key, value, timestamp_ms)
        except :
            self._logger.excpt()
            response = Response()
            response.status_code = RC_INTERNAL_ERROR
            t, v, tb = sys.exc_info()
            response.error = '%s -- "%s"' % (str(t), str(v))
            return response

        if not ok :
            response.status_code = RC_INTERNAL_ERROR
            response.error = 'unknown error -- op-code:%d data:%r' % (op, args)

        #self._logger.debug('Insert:%s done.',repr(args))
        return Response()
        ## __doInsertOrUpdate()

    def erase(self, args) :
        #self._logger.debug('Erase:%s',repr(args))
        table_name, key, timestamp_ms, db_path = args

        # STEP. get celldb
        celldb,cell_key_begin,cell_key_end = self.getCellDB( db_path )

        # STEP. check key interval
        if not (key > cell_key_begin and key <= cell_key_end) :
            response = Response(RC_OUT_OF_CELL_INTERVAL)
            response.error = 'Key %s is out of interval of table "%s" cell (%s .. %s]' % (repr(key),table_name,repr(cell_key_begin),repr(cell_key_end))
            return response

        # STEP. check whether to split celldb or not
        splitted = self.checkSplitableCell( celldb, table_name, db_path )
        if splitted :
            # if split occured, re-check again
            celldb,cell_key_begin,cell_key_end = self.getCellDB( db_path )

            if not (key > cell_key_begin and key <= cell_key_end) :
                response = Response(RC_OUT_OF_CELL_INTERVAL)
                response.error = 'Key %s is out of interval of table "%s" cell (%s .. %s]' % (repr(key),table_name,repr(cell_key_begin),repr(cell_key_end))
                self._logger.error('%s', response.error)
                return response
        
        # STEP. execute insert operation on celldb
        try :
            ok = celldb.erase(key, timestamp_ms)
        except :
            self._logger.excpt()
            response = Response()
            response.status_code = RC_INTERNAL_ERROR
            t, v, tb = sys.exc_info()
            response.error = '%s -- "%s"' % (str(t), str(v))
            return response

        if not ok :
            response.status_code = RC_INTERNAL_ERROR
            response.error = 'unknown ERASE error -- data:%r' % (op, args)

        #self._logger.debug('Erase:%s done.',repr(args))
        return Response()
        ## erase()

    def find(self, args) :
        #self._logger.debug('Find:%s',repr(args))
        table_name, key, timestamp_ms, db_path = args

        # STEP. get celldb
        celldb,cell_key_begin,cell_key_end = self.getCellDB( db_path )

        # STEP. check key interval
        if not (key > cell_key_begin and key <= cell_key_end) :
            response = Response(RC_OUT_OF_CELL_INTERVAL)
            response.error = 'Key %s is out of interval of table "%s" cell (%s .. %s]' % (repr(key),table_name,repr(cell_key_begin),repr(cell_key_end))
            return response

        # STEP. execute insert operation on celldb
        try :
            result = celldb.find(key, timestamp_ms)
        except :
            self._logger.excpt()
            response = Response()
            response.status_code = RC_INTERNAL_ERROR
            t, v, tb = sys.exc_info()
            response.error = '%s -- "%s"' % (str(t), str(v))
            return response

        #self._logger.debug('Find:%s done.',repr(args))
        if not result :
            self._logger.error('Empty of key %s in table "%s"', repr(key), table_name)
        response = Response()
        response.body = result
        return response
        ## find()
    
    ## class Worker


